package dbops

import (
	"context"
	"fmt"

	commonConstant "algo-sdk-go/common/constant"
	commonTools "algo-sdk-go/common/tools"

	"github.com/pkg/errors"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	mongodriver "go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

// struct
type FilterArr struct {
	Arr []bson.M
}
type PipelineArr struct {
	Pipeline   []bson.M
	Match      bson.M
	Unwind     []string
	Group      bson.M
	OpsHandler map[string]PipelineHandlerFunc
}
type Updater struct {
	Updater     bson.M
	SetOnInsert bson.M
	SwitchMap   map[string]bool
}
type Opts struct {
	Opts *options.FindOptions
}
type SortBy struct {
	SortKey string
	SortWay commonConstant.DBSortWay
}

// Func
type FilterFunc func(*FilterArr)
type PipelineFunc func(*PipelineArr)
type PipelineHandlerFunc func(*PipelineArr) *PipelineArr
type UpdaterFunc func(updater *Updater)
type OptsFunc func(updater *Opts)
type ErrFunc func() error

// default
func defaultFilter() *FilterArr {
	return &FilterArr{
		Arr: make([]bson.M, 0),
	}
}
func defaultPipeline() *PipelineArr {
	return &PipelineArr{
		Pipeline: make([]bson.M, 0),
		Match:    bson.M{},
		Group:    bson.M{},
		OpsHandler: map[string]PipelineHandlerFunc{
			commonConstant.MatchOps:  MatchOps,
			commonConstant.GroupOps:  GroupOps,
			commonConstant.UnwindOps: UnwindOps,
		},
	}
}
func defaultUpdater() *Updater {
	return &Updater{
		Updater:     bson.M{},
		SetOnInsert: bson.M{},
	}
}
func defaultOpts() *Opts {
	return &Opts{
		Opts: &options.FindOptions{},
	}
}

// Get
func GetPipeline(ops ...PipelineFunc) *PipelineArr {
	o := defaultPipeline()
	for _, op := range ops {
		op(o)
	}
	return o
}
func GetFilter(ops ...FilterFunc) *FilterArr {
	o := defaultFilter()
	for _, op := range ops {
		op(o)
	}
	return o
}
func GetUpdater(ops ...UpdaterFunc) *Updater {
	updater := defaultUpdater()
	for _, op := range ops {
		op(updater)
	}
	return updater
}
func SetOnInsert(key string, value interface{}) UpdaterFunc {
	return func(f *Updater) {
		f.SetOnInsert[key] = value
	}
}
func GetOpts(options ...OptsFunc) *Opts {
	defaultOpt := defaultOpts()
	for _, op := range options {
		op(defaultOpt)
	}
	return defaultOpt
}
func Valid(ops ...ErrFunc) error {
	for _, op := range ops {
		if err := op(); err != nil {
			return err
		}
	}
	return nil
}

func (f *FilterArr) Default(defaultFunc FilterFunc) *FilterArr {
	if f.Illegal() {
		defaultFunc(f)
	}
	return f
}
func (f *FilterArr) AndOperator() bson.M {
	return bson.M{commonConstant.DBAnd: f.Arr}
}
func (f *FilterArr) Illegal() bool {
	return f == nil || len(f.Arr) == 0
}
func (f *FilterArr) OrOperator() bson.M {
	return bson.M{commonConstant.DBOr: f.Arr}
}
func (f *Updater) Set() bson.M {
	return bson.M{commonConstant.DBSet: f.Updater}
}
func (f *Updater) DBSetOnInsert() bson.M {
	return bson.M{
		commonConstant.DBSet:         f.Updater,
		commonConstant.DBSetOnInsert: f.SetOnInsert,
	}
}
func (o *Opts) Get() *options.FindOptions {
	return o.Opts
}

// PipelineArr
func MatchOps(p *PipelineArr) *PipelineArr {
	if len(p.Match) != 0 {
		p.Pipeline = append(p.Pipeline, bson.M{
			commonConstant.DBMatch: p.Match,
		})
	}
	return p
}

// PipelineArr - match
func GetPipelineMatch(ops ...PipelineFunc) map[string]interface{} {
	o := defaultPipeline()
	for _, op := range ops {
		op(o)
	}
	return o.Match
}
func SetMatch(key string, values bson.M) func(f *PipelineArr) {
	return func(p *PipelineArr) {
		key = commonTools.TrimSpace(key)
		if key != "" && len(values) != 0 {
			p.Match[key] = values
		}
	}
}
func SetMatchValues(key string, values []string) func(f *PipelineArr) {
	return func(p *PipelineArr) {
		key = commonTools.TrimSpace(key)
		values = commonTools.UniqueAndFilter(values, commonTools.StringCast, commonTools.StringNotBlank)
		if key != "" && len(values) != 0 {
			p.Match[key] = bson.M{commonConstant.DBIn: values}
		}
	}
}
func SetMatchValue(key string, value string) func(f *PipelineArr) {
	return func(p *PipelineArr) {
		key = commonTools.TrimSpace(key)
		value = commonTools.TrimSpace(value)
		if key != "" && value != "" {
			p.Match[key] = value
		}
	}
}

// PipelineArr - group
func GroupOps(p *PipelineArr) *PipelineArr {
	if len(p.Group) != 0 {
		p.Pipeline = append(p.Pipeline, bson.M{
			commonConstant.DBGroup: p.Group,
		})
	}
	return p
}
func SetGroup(key string, values bson.M) func(f *PipelineArr) {
	return func(p *PipelineArr) {
		key = commonTools.TrimSpace(key)
		if key != "" && len(values) != 0 {
			p.Group[key] = values
		}
	}
}
func SetGroupValue(key string, value string) func(f *PipelineArr) {
	return func(p *PipelineArr) {
		key = commonTools.TrimSpace(key)
		value = commonTools.TrimSpace(value)
		if key != "" && value != "" {
			p.Group[key] = value
		}
	}
}
func GetPipelineGroup(ops ...PipelineFunc) map[string]interface{} {
	o := defaultPipeline()
	for _, op := range ops {
		op(o)
	}
	return o.Group
}

func UnwindOps(p *PipelineArr) *PipelineArr {
	for _, unwind := range p.Unwind {
		p.Pipeline = append(p.Pipeline, bson.M{
			commonConstant.DBUnwind: unwind,
		})
	}
	return p
}
func (p *PipelineArr) Aggregate(sortList []string) []bson.M {
	for _, sort := range sortList {
		handler, exist := p.OpsHandler[sort]
		if !exist {
			continue
		}
		handler(p)
	}
	return p.Pipeline
}

// OptsFunc
func SetProjection(projection interface{}) OptsFunc {
	return func(opt *Opts) {
		opt.Opts.Projection = projection
	}
}
func SetSortBy(sortBy []SortBy, defaultSortKet string) OptsFunc {
	return func(opt *Opts) {
		sort := bson.M{}
		for _, way := range sortBy {
			sort[way.SortKey] = way.SortWay.Value()
		}
		if len(sortBy) == 0 {
			sort[defaultSortKet] = commonConstant.DBSortDesc.Value()
		}
		opt.Opts.Sort = sort
	}
}
func SetPageNumPageSize(pageNum, pageSize int, ignore bool) OptsFunc {
	if ignore {
		return func(opt *Opts) {
			opt.Opts.Skip = nil
			opt.Opts.Limit = nil
		}
	}
	// 基础底层的数据查询保护，因此需要设置查询限制，如果业务上需要查更多的数据，可以换其他方法，但是建议不要修改 MaxPageSize
	pageNumInt := commonTools.TernaryExpression(pageNum-1 <= commonConstant.DefaultPageNum, commonConstant.DefaultPageNum, pageNum-1)
	pageSize = commonTools.TernaryExpression(pageSize > commonConstant.MaxPageSize, commonConstant.MaxPageSize, commonTools.TernaryExpression(pageSize <= commonConstant.ErrorPageSize, commonConstant.DefaultPageSize, pageSize))
	return func(opt *Opts) {
		pageSizeInt := int64(pageSize)
		skip := int64(pageNumInt) * pageSizeInt
		opt.Opts.Skip = &skip
		opt.Opts.Limit = &pageSizeInt
	}
}

// FilterFunc
func WithStringValue(dbKey, dbValue string) FilterFunc {
	return func(f *FilterArr) {
		dbKey = commonTools.TrimSpace(dbKey)
		dbValue = commonTools.TrimSpace(dbValue)
		if dbKey != "" && dbValue != "" {
			f.Arr = append(f.Arr, bson.M{dbKey: dbValue})
		}
	}
}
func WithStringValues(dbKey string, dbValues []string) FilterFunc {
	return func(f *FilterArr) {
		dbKey = commonTools.TrimSpace(dbKey)
		dbValues = commonTools.Filter(commonTools.Cast(dbValues, commonTools.TrimSpace), commonTools.StringNotBlank)
		if len(dbValues) != 0 {
			f.Arr = append(f.Arr, bson.M{dbKey: bson.M{commonConstant.DBIn: dbValues}})
		}
	}
}
func WithIntValue(dbKey string, dbValue int) FilterFunc {
	return func(f *FilterArr) {
		dbKey = commonTools.TrimSpace(dbKey)
		if dbKey != "" {
			f.Arr = append(f.Arr, bson.M{dbKey: dbValue})
		}
	}
}
func MatchFuzzyWord(dbColumnName string, fuzzyMatchWords []string) FilterFunc {
	return func(f *FilterArr) {
		var (
			fuzzyWords = make([]bson.M, 0)
		)
		fuzzyMatchWords = commonTools.Filter(commonTools.Cast(fuzzyMatchWords, commonTools.TrimSpace), commonTools.StringNotBlank)
		if dbColumnName == "" || len(fuzzyMatchWords) == 0 {
			return
		}

		for _, fuzzyWord := range fuzzyMatchWords {
			fuzzyWords = append(fuzzyWords, bson.M{dbColumnName: bson.M{commonConstant.DBRegex: primitive.Regex{Pattern: ".*" + fuzzyWord + ".*", Options: commonConstant.DBI}}})
		}
		f.Arr = append(f.Arr, bson.M{commonConstant.DBOr: fuzzyWords})
	}
}
func MatchFuzzyWordWithMultiColumn(fuzzyWord string, dbMultiColumn []string) FilterFunc {
	return func(f *FilterArr) {
		var (
			fuzzyWords = make([]bson.M, 0)
		)
		dbMultiColumn = commonTools.Filter(commonTools.Cast(dbMultiColumn, commonTools.TrimSpace), commonTools.StringNotBlank)
		fuzzyWord = commonTools.TrimSpace(fuzzyWord)
		if fuzzyWord == "" || len(dbMultiColumn) == 0 {
			return
		}
		for _, dbColumn := range dbMultiColumn {
			fuzzyWords = append(fuzzyWords, bson.M{
				dbColumn: bson.M{commonConstant.DBRegex: primitive.Regex{Pattern: ".*" + fuzzyWord + ".*", Options: commonConstant.DBI}},
			})
		}
		f.Arr = append(f.Arr, bson.M{commonConstant.DBOr: fuzzyWords})
	}
}
func WithoutDelete() FilterFunc {
	return func(f *FilterArr) {
		f.Arr = append(f.Arr, bson.M{commonConstant.DBIsDeleted: false})
	}
}
func WithoutDeleteKey(key string) FilterFunc {
	return func(f *FilterArr) {
		f.Arr = append(f.Arr, bson.M{commonTools.TernaryExpression(key != "", key, commonConstant.DBIsDeleted): false})
	}
}
func WithRange(dbColumn string, min, max int) FilterFunc {
	return func(f *FilterArr) {
		if min > 0 {
			f.Arr = append(f.Arr, bson.M{dbColumn: bson.M{commonConstant.DBGte: min}})
		}
		if max > 0 && max >= min {
			f.Arr = append(f.Arr, bson.M{dbColumn: bson.M{commonConstant.DBLte: max}})
		}
	}
}

// UpdaterFunc
func SetSwitchMap(switchMap map[string]bool) UpdaterFunc {
	return func(f *Updater) {
		f.SwitchMap = switchMap
	}
}
func SetIntValue(dbKey string, value int) UpdaterFunc {
	return func(f *Updater) {
		flag := true
		dbKey = commonTools.TrimSpace(dbKey)
		if f.SwitchMap != nil {
			flag = f.SwitchMap[dbKey]
		}
		if dbKey != "" && value > 0 && flag {
			f.Updater[dbKey] = value
		}
	}
}
func SetStringValue(dbKey, value string) UpdaterFunc {
	return func(f *Updater) {
		flag := true
		dbKey = commonTools.TrimSpace(dbKey)
		value = commonTools.TrimSpace(value)
		if f.SwitchMap != nil {
			flag = f.SwitchMap[dbKey]
		}
		if dbKey != "" && value != "" && flag {
			f.Updater[dbKey] = value
		}
	}
}
func SetValue(dbKey string, value interface{}) UpdaterFunc {
	return func(f *Updater) {
		flag := true
		dbKey = commonTools.TrimSpace(dbKey)
		if f.SwitchMap != nil {
			flag = f.SwitchMap[dbKey]
		}
		if dbKey != "" && value != "" && flag {
			f.Updater[dbKey] = value
		}
	}
}

// ErrFunc
func ValidMaxLength[T any](list []T, maxLength int) ErrFunc {
	return func() error {
		if len(list) > maxLength {
			return errors.New(fmt.Sprintf("length of list over %d", maxLength))
		}
		return nil
	}
}
func LengthMustNotZero[T any](list []T) ErrFunc {
	return func() error {
		if len(list) == 0 {
			return errors.New("query list length is zero")
		}
		return nil
	}
}
func ValueNotEmpty(value string, errMsg string) ErrFunc {
	return func() error {
		if value == "" {
			return errors.New(errMsg)
		}
		return nil
	}
}
func EmptyOps() ErrFunc {
	return func() error {
		return nil
	}
}
func ValuesNotEmpty(valueList []string, errMsg string) ErrFunc {
	return func() error {
		valueList = commonTools.Filter(commonTools.Cast(valueList, commonTools.TrimSpace), commonTools.StringNotBlank)
		if len(valueList) == 0 {
			return errors.New(errMsg)
		}
		return nil
	}
}
func ValuesNotAllowEmpty[T any](valueList []T, errMsg string) ErrFunc {
	return func() error {
		if len(valueList) == 0 {
			return errors.New(errMsg)
		}
		return nil
	}
}

func AggregateItemsFromMongo[T, K any](ctx context.Context, collection *mongodriver.Collection, pipeline []bson.M, opts *options.AggregateOptions, convertor func(T) K) (items []K, err error) {
	var (
		itemListFromDB []T
	)

	cursor, err := collection.Aggregate(ctx, pipeline, opts)
	if err != nil {
		return nil, nil
	}

	defer cursor.Close(ctx)
	if err = cursor.All(ctx, &itemListFromDB); err != nil {
		return nil, nil
	}
	return commonTools.Cast(itemListFromDB, convertor), nil
}

func GetAllItemsFromMongo[T, K any](ctx context.Context, collection *mongodriver.Collection, filter bson.M, opts *options.FindOptions, convertor func(T) K) (items []K, err error) {
	var (
		itemListFromDB []T
	)

	cursor, err := collection.Find(ctx, filter, opts)
	if err != nil {
		return nil, errors.Wrapf(err, "find_err, filter: %s", commonTools.JsonToString(filter))
	}

	defer cursor.Close(ctx)
	if err = cursor.All(ctx, &itemListFromDB); err != nil {
		return nil, errors.Wrapf(err, "find_all_err, filter: %s", commonTools.JsonToString(filter))
	}

	return commonTools.Cast(itemListFromDB, convertor), nil
}
